package jexxus.server;

import java.io.IOException;
import java.net.BindException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;

import javax.net.ServerSocketFactory;

import jexxus.common.Connection;
import jexxus.common.ConnectionListener;

/**
 * Acts as a server for incoming client connections. The server can send and
 * receive data from all clients who connect to this server.
 */
public class Server
{
    private final static int                          NO_UDP_PORT_VALUE       = -1;

    private ConnectionListener                        mServerListener;
    private ServerSocket                              mTcpSocket;
    private DatagramSocket                            mUdpSocket;
    private boolean                                   mRunning                = false;
    protected final int                               mTcpPort;
    protected final int                               mUdpPort;
    private final HashMap<String, ConnectionToClient> mClients                = new HashMap<String, ConnectionToClient>();
    private final HashMap<String, ConnectionToClient> mUdpClients             = new HashMap<String, ConnectionToClient>();
    private final IServerForClients                   mServerForClients       = new ServerForClients();

    private final DatagramPacket                      mOutgoingPacket         = new DatagramPacket(new byte[0], 0);

    private final IServerForClients                   NULL_SERVER_FOR_CLIENTS = new IServerForClients()
                                                                              {
                                                                                  @Override
                                                                                  public void sendUDP( byte[] pSentData,
                                                                                          ConnectionToClient pSendingConnectionToClient )
                                                                                  {
                                                                                  }

                                                                                  @Override
                                                                                  public void connectionDied(
                                                                                          ConnectionToClient pLostConnectionToClient,
                                                                                          boolean pForced )
                                                                                  {
                                                                                  }
                                                                              };
    /**
     * Listener which does nothing. It is used as long as there is no real
     * listener being set.
     */
    private static final ConnectionListener           NULL_LISTENER           = new ConnectionListener()
                                                                              {
                                                                                  @Override
                                                                                  public void receive( byte[] pData, Connection pFrom )
                                                                                  {
                                                                                  }

                                                                                  @Override
                                                                                  public void connectionBroken( Connection pBroken,
                                                                                          boolean pForced )
                                                                                  {
                                                                                  }

                                                                                  @Override
                                                                                  public void clientConnected( ConnectionToClient pConn )
                                                                                  {
                                                                                  }
                                                                              };

    /**
     * Creates a new server. This can be used, if one wants to inject the
     * connection-listener later.
     * 
     * @param port
     *            The port to listen for client connections on. [TCP]
     */
    public Server(int port)
    {
        this(NULL_LISTENER, port);
    }

    /**
     * Creates a new server.
     * 
     * @param listener
     *            The responder to special events such as receiving data.
     * @param port
     *            The port to listen for client connections on. [TCP]
     */
    public Server(ConnectionListener listener, int port)
    {
        this(listener, port, NO_UDP_PORT_VALUE);
    }

    /**
     * Creates a new server.<br>
     * <br>
     * Note: The server will not begin listening for connections until
     * <code>startServer()</code> is called.
     * 
     * @param listener
     *            The responder to special events such as receiving data.
     * @param tcpPort
     *            The port to listen for TCP client connections on.
     * @param udpPort
     *            The port to listen for UDP client connections on. Use -1 if
     *            you don't want to use any UDP.
     */
    public Server(ConnectionListener listener, int tcpPort, int udpPort)
    {
        this.mServerListener = listener;

        this.mTcpPort = tcpPort;
        this.mUdpPort = udpPort;
    }

    /**
     * After the server has started, it is open for accepting new client
     * connections.
     */
    public synchronized void startServer()
    {
        if( mRunning )
        {
            System.err.println("Cannot start server when already running!");
            return;
        }
        mRunning = true;

        try
        {
            ServerSocketFactory socketFactory = ServerSocketFactory.getDefault();
            mTcpSocket = socketFactory.createServerSocket(mTcpPort);
        }
        catch( BindException e )
        {
            System.err.println("There is already a server bound to port " + mTcpPort + " on this computer.");
            throw new RuntimeException(e);
        }
        catch( IOException e )
        {
            if( e.toString().contains("JVM_Bind") )
            {
                System.err.println("There is already a server bound to port " + mTcpPort + " on this computer.");
            }
            throw new RuntimeException(e);
        }
        if( mUdpPort != NO_UDP_PORT_VALUE )
        {
            try
            {
                mUdpSocket = new DatagramSocket(mUdpPort);
            }
            catch( SocketException e )
            {
                System.err.println("There was a problem starting the server's UDP socket on port " + mUdpPort);
                System.err.println(e.toString());
            }
        }

        startTCPConnectionListener();
        if( mUdpPort != NO_UDP_PORT_VALUE )
        {
            startUDPListener();
        }
    }

    private void startTCPConnectionListener()
    {
        Thread t = new Thread(new Runnable()
        {
            public void run()
            {
                while( mRunning )
                {
                    try
                    {
                        Socket sock = mTcpSocket.accept();

                        ConnectionToClient newClient = new ConnectionToClient(mServerForClients, mServerListener, sock);
                        synchronized( mClients )
                        {
                            mClients.put(newClient.getIP(), newClient);
                        }
                        mServerListener.clientConnected(newClient);
                    }
                    catch( IOException e )
                    {
                        if( mRunning )
                        {
                            e.printStackTrace();
                        }
                        break;
                    }
                }
            }
        });
        t.setName("Jexxus-TCPConnectionListener");
        t.start();
    }

    private void startUDPListener()
    {
        Thread t = new Thread(new Runnable()
        {
            public void run()
            {
                final int BUF_SIZE = 2048;
                final DatagramPacket inputPacket = new DatagramPacket(new byte[BUF_SIZE], BUF_SIZE);
                while( true )
                {
                    try
                    {
                        mUdpSocket.receive(inputPacket);
                        byte[] ret = Arrays.copyOf(inputPacket.getData(), inputPacket.getLength());
                        String senderIP = inputPacket.getAddress().getHostAddress();
                        ConnectionToClient conn = mUdpClients.get(senderIP + inputPacket.getPort());
                        if( conn == null )
                        {
                            synchronized( mClients )
                            {
                                conn = mClients.get(senderIP);
                            }
                        }
                        if( conn == null )
                        {
                            System.err.println("Received UDP Packet from unknown source: " + senderIP);
                        }
                        else
                        {
                            if( ret.length == 0 )
                            {
                                System.out.println("Set UDP Port: " + inputPacket.getPort());
                                if( conn.getUDPPort() != NO_UDP_PORT_VALUE )
                                {
                                    // see if there is another connection
                                    // without a UDP port set
                                    for( ConnectionToClient sc : mClients.values() )
                                    {
                                        if( sc.getUDPPort() == NO_UDP_PORT_VALUE )
                                        {
                                            conn = sc;
                                            break;
                                        }
                                    }
                                }
                                conn.setUDPPort(inputPacket.getPort());
                                mUdpClients.put(senderIP + inputPacket.getPort(), conn);
                            }
                            else
                            {
                                mServerListener.receive(ret, conn);
                            }
                        }
                    }
                    catch( IOException e )
                    {
                        if( mRunning )
                        {
                            System.err.println("UDP Socket failed!");
                            mRunning = false;
                        }
                        break;
                    }
                }
            }
        });
        t.start();
    }

    private class ServerForClients
            implements IServerForClients
    {
        @Override
        public void connectionDied( final ConnectionToClient pLostConnectionToClient, final boolean pForced )
        {
            synchronized( mClients )
            {
                mClients.remove(pLostConnectionToClient.getIP());
            }
            synchronized( mUdpClients )
            {
                mUdpClients.remove(pLostConnectionToClient.getIP() + pLostConnectionToClient.getUDPPort());
            }
            mServerListener.connectionBroken(pLostConnectionToClient, pForced);
        }

        @Override
        public void sendUDP( final byte[] pSentData, final ConnectionToClient pSendingConnectionToClient )
        {
            synchronized( mOutgoingPacket )
            {
                mOutgoingPacket.setData(pSentData);
                mOutgoingPacket.setAddress(pSendingConnectionToClient.getAddress());
                mOutgoingPacket.setPort(pSendingConnectionToClient.getUDPPort());
                try
                {
                    mUdpSocket.send(mOutgoingPacket);
                }
                catch( IOException e )
                {
                    e.printStackTrace();
                }
            }
        }
    }

    public synchronized void shutdown( final boolean pIgnoreDisconnectingMessagesFromClients )
    {
        if( !mRunning )
        {
            System.out.println("No need to stop server, because its not running.");
            return;
        }
        mRunning = false;

        synchronized( mClients )
        {
            for( ConnectionToClient currentConnectionToClient : mClients.values() )
            {
                if( pIgnoreDisconnectingMessagesFromClients )
                {
                    currentConnectionToClient.setServerForClients(NULL_SERVER_FOR_CLIENTS);
                }

                currentConnectionToClient.exit();
                mClients.clear();
            }
        }

        try
        {
            mTcpSocket.close();
        }
        catch( IOException e )
        {
            e.printStackTrace();
        }
        if( mUdpSocket != null )
        {
            mUdpSocket.close();
        }
    }

    /**
     * After the server has shut down, no new client connections can be
     * established.
     */
    public synchronized void shutdown()
    {
        final boolean DEFAULT_IGNORE_MESSAGES_FROM_CLIENTS_AFTER_SHUTDOWN = true;
        shutdown(DEFAULT_IGNORE_MESSAGES_FROM_CLIENTS_AFTER_SHUTDOWN);
    }

    public synchronized boolean isRunning()
    {
        return mRunning;
    }

    public List<Connection> getClients()
    {
        List<Connection> connectedClients = new ArrayList<>();

        synchronized( mClients )
        {
            connectedClients.addAll(mClients.values());
        }

        return connectedClients;
    }

    /**
     * Sets a new connection-listener.
     * 
     * @param newConnectionListener
     *            This is the new connection-listener.
     */
    public synchronized void setConnectionListener( final ConnectionListener newConnectionListener )
    {
        this.mServerListener = newConnectionListener;
    }

}
